ALBのバックエンドで起動するLambdaが複数ある場合、CloudFormationテンプレートはどのリソース単位で分けると都合が良いのか考えてみた
ALBのバックエンドには処理毎に分れたLambdaが複数あります。ALBと複数のLambdaを1つのCloudFormationテンプレートにまとめることもできます。しかし、Lambda毎に開発しているチームが異なるなどでLambda単位で管理を分けたいです。ALBは独立して管理し、Lambdaは各開発チームで管理できるようにしたい場合、どこのリソースでテンプレートを分割すると都合がよいのか考えてみました。SAMでLambdaをデプロイする構成の一例として紹介します。
ALBはCloudFormationのテンプレートで管理し、各種LambdaはSAMテンプレートで管理しAWS SAMを利用してデプロイする状況を想定します。
まとめ
SAMで管理する個々のLambdaをELBのバックエンドに追加していくことが予想される場合、以下の箇所でテンプレートを区切りました。
- ALB本体とリスナーのベースとなる部分
- 各Lambda関数とリスナールール、ターゲットグループ
sam deploy
で作成したLambdaを既存ELBのリスナーに紐づけていけることを確認しました。
テンプレートの単位
青、緑色背景は個々にSAMで管理する単位、それ以外はALBのCloudFormationテンプレートで管理。
検証環境
項目 | 値 |
---|---|
AWS SAM | 1.36.0 |
本検証で使用したALBのCloudFormationテンプレート、SAMで管理するLambda一式は以下においてあります。
考えてみた
ALBのバックエンドで起動するLambdaの構成は構成図でみる分にはシンプルです。ALBと各Lambda単位にテンプレート分ければ済みそうと一見思えます。
もう少し細かくみたいのでリソース単位に分解します。こうするとALBに関連するリソースが地味にあって悩ましくなってきますリスナー、リスナールール、ターゲットグループ、この辺りのまとめ方が非常に悩ましいです。
SAMでLambdaをデプロイすることを考慮した結果
- ALB本体とリスナー
- Lambda関数とリスナールール、ターゲットグループ
上記のリソースを1単位として管理するとキレイな分け方ができるのではないかと思い、sam deploy
で既存ALBにLambdaの紐付け実現できるのか確認してみました。
ELBとリスナー作成
ELB本体とLambdaを紐付けていくリスナーをCloudFormationで作成します。リスナーのデフォルトルールはELB側に持たせます。 作成したリスナーはエクスポートし、Lambdaを作成するSAMテンプレートでクロススタック参照して利用することにします。
AWSTemplateFormatVersion: "2010-09-09" Description: ALB*1 Parameters: ProjectName: Description: Project Name Type: String Default: unnamed Environment: Description: Environment Type: String Default: dev AllowedValues: - prod - dev - stg VPCID: Type: AWS::EC2::VPC::Id PublicSubnet1: Description: "ELB Subnet 1st" Type: AWS::EC2::Subnet::Id PublicSubnet2: Description: "ELB Subnet 2nd" Type: AWS::EC2::Subnet::Id Resources: # -------------------------------------------- # ELB # -------------------------------------------- ELB1: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Type: "application" Name: !Sub ${ProjectName}-${Environment}-elb Scheme: "internet-facing" SecurityGroups: - !Ref SecurityGroup1 Subnets: - !Ref PublicSubnet1 - !Ref PublicSubnet2 IpAddressType: "ipv4" LoadBalancerAttributes: - Key: "deletion_protection.enabled" Value: "false" Tags: - Key: Name Value: !Sub ${ProjectName}-${Environment}-elb ELBListener1: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: fixed-response FixedResponseConfig: StatusCode: 404 MessageBody: Not Found. ContentType: text/plain LoadBalancerArn: !Ref ELB1 Port: 80 Protocol: HTTP SecurityGroup1: Type: AWS::EC2::SecurityGroup Properties: GroupName: !Sub ${ProjectName}-${Environment}-elb-sg GroupDescription: ELB Security Group SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: "0.0.0.0/0" Description: "Access from Public" VpcId: !Ref VPCID Tags: - Key: Name Value: !Sub ${ProjectName}-${Environment}-elb-sg Outputs: Listener1: Description: "Linstener ARN" Value: !GetAtt ELBListener1.ListenerArn Export: Name: !Sub ${AWS::StackName}-Listener1
ベースとなるリスナーが作成されました。
リスナールールは固定レスポンスを返すデフォルトルールのみ作成しています。
アクセステスト
ELBにアクセスすると固定レスポンスのNot Found.
が返ってくるだけのELBが完成しました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com Not Found.⏎
1個目のLambda作成
LambdaはSAMで管理します。Lambda関数本体と、リスナールール、ターゲットグループを同時に作成します。その他、ELBからLambdaを呼び出すために必要なリソースベースポリシー(AWS::Lambda::Permission
)も作成しLambdaに設定します。そして、SAMテンプレートで新規作成するリスナールールを既存リスナーに紐付けることでELBと連携させます
ポイント
- リスナールールの
Priority
は他のLambdaをデプロイするときに重複しないように管理が必要
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > first-lambda Sample SAM Template for first-lambda Resources: Function: Type: AWS::Serverless::Function Properties: FunctionName: "first-lambda" CodeUri: ./first-lambda Handler: app.lambda_handler Runtime: python3.9 Architectures: - arm64 Timeout: 5 LambdaPermission: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt Function.Arn Principal: "elasticloadbalancing.amazonaws.com" SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*" # --------------------------- # Resources associated with the ALB # --------------------------- TargetGroup1: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: HealthCheckIntervalSeconds: 15 HealthCheckPath: "/" HealthCheckTimeoutSeconds: 10 UnhealthyThresholdCount: 2 TargetType: "lambda" Matcher: HttpCode: "200" HealthyThresholdCount: 2 Name: "first-lambda-tg" HealthCheckEnabled: false TargetGroupAttributes: - Key: "lambda.multi_value_headers.enabled" Value: "false" Targets: - Id: !GetAtt Function.Arn AvailabilityZone: "all" ListenerRule: Type: "AWS::ElasticLoadBalancingV2::ListenerRule" Properties: Priority: "1" ListenerArn: !ImportValue blog-alb-Listener1 Conditions: - Field: "path-pattern" Values: - "/v1/lambda1" Actions: - Type: "forward" TargetGroupArn: !Ref TargetGroup1 Order: 1 ForwardConfig: TargetGroups: - TargetGroupArn: !Ref TargetGroup1 Weight: 1 TargetGroupStickinessConfig: Enabled: false Outputs: Function: Description: "Lambda Function ARN" Value: !GetAtt Function.Arn FunctionIamRole: Description: "Implicit IAM Role created for function" Value: !GetAtt FunctionRole.Arn
ELBから呼び出されるLambdaはメッセージをレスポンスに返すだけです。
import json def lambda_handler(event, context): return { "statusCode": 200, "body": json.dumps({ "message": "I am FIRST Lambda back of ALB. ", }), }
既存リスナーに新規作成のリスナールールとLambdaを作成するSAMテンプレートをビルドしてAWSへデプロイします。
$ sam build ...snip... $ sam deploy --guided ...snip... Stack Name [sam-app]: first-lambda AWS Region [us-east-1]: ap-northeast-1 #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: n Save arguments to configuration file [Y/n]: y SAM configuration file [samconfig.toml]: SAM configuration environment [default]: ...snip... Successfully created/updated stack - first-lambda in ap-northeast-1
デプロイが完了すると、既存リスナーに新たなリスナールールが追加されました。
アクセステスト
ALBのURLに対してリスナールールで指定したパス(/v1/lambda1
)にアクセスすると、Lambdaからレスポンスが返ってきます。既存のALBにsam deploy
したLambdaを上手いこと連携できました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda1 {"message": "I am FIRST Lambda back of ALB. "}⏎
2個目のLambda作成
重要なところはリスナールールのプライオリティを重複させないことです。1個目のLambdaとほぼ同様のSAMテンプレートを作成しました。2個目のLambdaを呼ぶパスはv1/lambda2
としました。
AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: > second-lambda Sample SAM Template for second-lambda Resources: Function: Type: AWS::Serverless::Function Properties: FunctionName: "second-lambda" CodeUri: ./second-lambda Handler: app.lambda_handler Runtime: python3.9 Architectures: - arm64 Timeout: 5 LambdaPermission: Type: "AWS::Lambda::Permission" Properties: Action: "lambda:InvokeFunction" FunctionName: !GetAtt Function.Arn Principal: "elasticloadbalancing.amazonaws.com" SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*" # --------------------------- # Resources associated with the Internal ALB # --------------------------- TargetGroup1: Type: "AWS::ElasticLoadBalancingV2::TargetGroup" Properties: HealthCheckIntervalSeconds: 15 HealthCheckPath: "/" HealthCheckTimeoutSeconds: 10 UnhealthyThresholdCount: 2 TargetType: "lambda" Matcher: HttpCode: "200" HealthyThresholdCount: 2 Name: "second-lambda-tg" HealthCheckEnabled: false TargetGroupAttributes: - Key: "lambda.multi_value_headers.enabled" Value: "false" Targets: - Id: !GetAtt Function.Arn AvailabilityZone: "all" ListenerRule: Type: "AWS::ElasticLoadBalancingV2::ListenerRule" Properties: Priority: "2" ListenerArn: !ImportValue blog-alb-Listener1 Conditions: - Field: "path-pattern" Values: - "/v1/lambda2" Actions: - Type: "forward" TargetGroupArn: !Ref TargetGroup1 Order: 1 ForwardConfig: TargetGroups: - TargetGroupArn: !Ref TargetGroup1 Weight: 1 TargetGroupStickinessConfig: Enabled: false Outputs: Function: Description: "Lambda Function ARN" Value: !GetAtt Function.Arn FunctionIamRole: Description: "Implicit IAM Role created for function" Value: !GetAtt FunctionRole.Arn
Lambdaのコードはレスポンスのメッセージを変更しただけなので省略します。リポジトリを参考ください。
またしても既存リスナーに新規のリスナールールと2個目のLambdaを作成するSAMテンプレートをビルドしてAWSへデプロイします。
$ sam build ...snip... $ sam deploy --guided ...snip... Stack Name [sam-app]: second-lambda AWS Region [us-east-1]: ap-northeast-1 #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: n Save arguments to configuration file [Y/n]: y SAM configuration file [samconfig.toml]: SAM configuration environment [default]: ...snip... Successfully created/updated stack - second-lambda in ap-northeast-1
デプロイが完了すると、2個目のリスナールールが新たに追加されました。
アクセステスト
ALBのURLに対して2個目のリスナールールで指定したパスにアクセスするとLambdaからのレスポンスが返ってきます。ALBから呼び出したいLambdaが増えた場合も対応していけそうです。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda2 {"message": "I am SECOND Lambda back of ALB!!!!!! "}⏎
まとめ
ELBのリスナーに対してSAM管理のLambdaを紐付けて、ELBのパス違いで別々のLabmdaを呼び出すことを確認できました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda1 {"message": "I am FIRST Lambda back of ALB. "}⏎ $ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda2 {"message": "I am SECOND Lambda back of ALB!!!!!! "}⏎
先にELBを作成しSAMで管理するLambdaを次々に生み出す場合は、以下の箇所でテンプレートを区切るとキレイに分けることができたのではないでしょうか。
- ALB本体とリスナーのベース部分
- 各Lambda関数とリスナールール、ターゲットグループ
おわりに
テンプレートで管理する単位はライフサイクルに寄ってきます。最適解は都度考えないといけないのですが、どのようなパターンが考えられるのかいくつかの参考例を見た上で検討したかったのですがサンプル数が少なかったので一例としてあげました。なにかの参考になれば幸いです。